<?php
/**
* EMAIL CLASS
*
* @package		Emails
* @author		$Author: cdburgess@gmail.com $
* @version		$Rev: 36 $
* @lastrevision	$Date: 2010-02-11 08:22:30 +0000 (Thu, 11 Feb 2010) $
* @filesource	$URL: http://blogchuck.googlecode.com/svn/trunk/emails.php $
* @license		$License: http://creativecommons.org/licenses/by-sa/3.0/$
* @copyright	$Copyright: (c)2010 Chuck Burgess. All Rights Reserved.$
*
* Please feel free to visit my blog http://blogchuck.com
* You can view my code repository at http://code.google.com/p/blogchuck/
*
* Description of Class, usage, and documentation can be found on the wiki: http://code.google.com/p/blogchuck/wiki/EmailsClass
* 
*/
class Emails extends Domains
{	
	// messaging for status
	private $invalid 	= 'The email address is not RFC compliant.';
	private $error 		= 'There was an error with your request.';
	private $not_tested = 'There was something wrong with the system.';
	private $valid 		= 'Email meets the RFC specifications.';
	private $verify		= 'Email meets the RFC specification and has been verified on the email server.';
	
	// holds the verification status
	public $status = array();
	
	/**
	 * ValidateDomain
	 *
	 * Basic rules of email validation
	 * - must have one @
	 * - total length must not exceed 256
	 * - must have a local part
	 * - must have a domain
	 * - domain must be valid
	 * - local part must be valid
	 */	
	function _checkEmail($email, $verify = false)
	{
		
		if(strlen($email) > 256 )
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Email length is greater than 256 charcters.');
			return false;	// cannot be more than 256 characters
		}
		
		// get the exact position of the @ symbol 
		$atIndex = strrpos($email,'@');	
		
        if ($atIndex === false)										
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Must have one @ symbol to be valid.');
			return false;											
		}
		
		if($atIndex === 0)											
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Local part of email is missing.');
			return false;											
		}
		
		if($atIndex === $emailLength - 1)      				 		
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Domain name is missing from email.');
			return false;
		}
		
		// split out the domain and the local part
		$local_part     = substr($email, 0, $atIndex);					// extract the local part of the email
        $domain         = substr($email, $atIndex + 1);					// extract the domain
		
		if(!$this->validateDomain($domain, $verify))	return false;	// requires a valid domain (status already set)
		if(!$this->_validateLocalpart($local_part))		return false;	// requires a valid local part (status already set)
		
		return true;
	}
	
	/**
	* Validate Local Part of Email
	* 
	* The local part that is RFC compliant, although technically compliant, may not necessarily pass through any
	* SMTP server as a valid email address. That being said, this needs to be the most compliant email validation
	* tool available for PHP. We conform to RFC 5322, specifically section 4 that identifies obsolete addressing.
	* This script will NOT valid obsolete or legacy addresses, but only addresses that can be generated as specified
	* by compliance to this RFC.
	*
	* Requirements (quoted from RFCs)
	* - postmaster local part is reserved (still valid)
	* - SHOULD avoid defining mailboxes where the Local-part requires (or uses) the Quoted-string form or where the Local-part is case-sensitive
	* - MUST NOT define mailboxes in such a way as to require the use in SMTP of non-ASCII characters 
	* - MUST NOT use ASCII "control characters" 
	* - obsoleted local part definitions to be ignored (means limited to dot-atoms only)
	* - the dot-atom form SHOULD be used and the quoted-string form SHOULD NOT be used
	* - dot-atom-text = 1*atext *("." 1*atext)
	* - atext can include: {ALPHA} {DIGIT} ! # $ % & ' * + - / = ? ^ _ ` { | } ~
	* - local-parts may consist of any combination of alphabetic characters, digits, or any of the special characters
	*      ! # $ % & ' * + - / = ?  ^ _ ` . { | } ~
	* - period (".") may also appear, but may not be used to start or end the local part, nor may two or more consecutive periods appear.
	* 
	*/
	function _validateLocalpart($local)
	{
		if(strlen($local) > 64 or strlen($local) < 1)						
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Local part of email cannot exceed 64 charcters.');
			return false;
		}
		
		if($local[0] == '.' || $local[$localLen-1] == '.')
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Local part of email cannot start or end with a dot.');
			return false;
		}
		
		if(preg_match('/\.\./', $local))
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Local part of email cannot contain two consecutive dots.');
			return false;
		}
		
		if(!preg_match("/^[\w!\#\$\%\&\'\*\+\-\/\=\?\^\`\.\{\|\}\~]+$/iD", $local))
		{
			$this->status = array(false, 'INVALID', $this->invalid, 'Local part of email cannot contain illegal characters.');
			return false;
		}
		
		return true;
	}
	
	/**
	 * Validate the email.
	 * Passing a second parameter of true will also verify via smtp
	 * 
	 * $email : the email address to be checked
	 * $verify : BOOL determines whether to verify email via SMTP
	 * $helo : the server the script is running on (example.com) - required for the HELO command
	 *
	 * =============================================================================================================
	 * Note: The $helo address may need to pass certain tests in order to communicate with various mail servers. 
	 * There may be policies on the email server that will prevent you from running checks based on specific
	 * policies. For example, Yahoo! uses spamhaus (spamhaus.org) to check the status of the IP address of your
	 * sending server. (http://www.spamhaus.org/query/bl?ip=192.168.0.1). If the IP address of your server is contained
	 * in any of their lists (SBL, PBL, XBL), then the request is immediately rejected and there is no way to get
	 * a response from the server as to the validity of the email address. To better understand how spamhaus works,
	 * see http://www.spamhaus.org/dnsbl_function.html.
	 * =============================================================================================================
	 *
	 * To avoid inacurate INVALID responses, this function will indicate how it failed. There are multiple
	 * reasons the test could fail:
	 * - invalid email address (no @ or determinable domain)
	 * - cannot connect to the internet (no test was actually performed)
	 * - sent request as if valid, but the server indicates the email is invalid
	 *
	 *
	 */
	function validateEmail($email, $verify = false, $helo = '')
	{
		
		// first validate the email address
		if(!$this->_checkEmail($email, $verify))									return false; 	// email address must be rfc compliant to pass (status is already set)
	
		// are we going to verify the address vis SMTP too?
		if($verify == true)												
		{
			
			// set $helo to this domain if one is not provided
			if(empty($helo)){
				// set the helo domain to this host name or IP
				if(!$this->thisHostName())
				{
					$this->status = array(false, 'NOT TESTED', "Valid host name could not be determined.", 'You need to pass the $helo option to identify the domain to use.');
					return false;
				}
				$helo = $this->this_server;	
			}
			
			// get the list of valid servers from the domains class
			if(empty($this->servers))
			{
				$this->status = array(false, 'NOT TESTED', "No mailservers were found in the DNS for $this->valid_domain .", null);
				return false;	
			}
			
			// this email address is required to exist on the server if the server is compliant
			$postmaster = 'postmaster@'.$this->valid_domain;
			
			// SMTP Commands
			$smtp_commands = array(
				"EHLO $helo",
				"MAIL FROM: <$postmaster>",
				"RCPT TO: <$email>",
				"QUIT"
			);
			
			// Process each mailserver until address is verified
			for($n=0; $n < count($this->servers); $n++)
			{
				$errno = 0; $errstr = 0;											// reset error data for each connection attempt
				
				# Try to open up socket
				if($sock = @fsockopen($this->servers[$n], 25, $errno , $errstr, $connect_timeout)) {
					
					$response = fgets($sock);										// get the initial response from the socket
					
					stream_set_timeout($sock, 30);									// give the stream 30 seconds to verify address
					$meta = stream_get_meta_data($sock);							// get the header/meta data from the pointers
					
					// make sure connection was initiated / no timeout
					if(!$meta['timed_out'] && !preg_match('/^2\d\d[ -]/', $response)) {				// did we connect to the server?
						
						// set the status in the case this is the last server tried
						$this->status = array(false, 'NOT TESTED', $not_tested, 'The fsockopen returned an error: $this->servers[$n] said: '.$response);
						break;																		// skip to the next server
					}
						
					// check the response of each command
					foreach($smtp_commands as $cmd) {
						if(!fputs($sock, "$cmd\r\n"))
						{
							$this->status = array(false, 'NOT TESTED', "The email is valid, but none of the mailservers listed for $this->valid_domain could not be contacted.", $errstr);
							return false;
						}
						
						$response = fgets($sock, 4096);				// get server response
						
						// error if thee is any response outside of 250 (meaning success)
						if(!$meta['timed_out'] && !preg_match('/^250[ -]/', $response)) {
						
							// EHLO command not accepted
							if($cmd == $cmds[0]){
								$this->status = array(false, 'INVALID', 'EHLO: Email meets the RFC specifications but is not valid on the server or cannot be verified.', $response);
							} 
							
							// VRFY returned error or this domain cannot verify forwarder
							if($cmd == $cmds[1]){
								$this->status = array(false, 'INVALID', 'MAIL FROM: Email meets the RFC specifications but is not valid on the server or cannot be verified.', $response);
							}
							
							// VRFY returned error or this domain cannot verify forwarder
							if($cmd == $cmds[2]){
								$this->status = array(false, 'INVALID', 'RCPT TO: Email meets the RFC specifications but is not valid on the server or cannot be verified.', $response);
							}
							
							// we already got an error on this server, so try the next in the loop
							break 2;
						} 
					}
					
					// close the connection
					fclose($sock);

					// if we get here, then the email is validated AND verified 
					$this->status =  array(true, 'VALID', $this->verify, $response);
					return true;
					
				} else {
					$this->status = array(false, 'NOT TESTED', "The email is valid, but none of the mailservers listed for $this->valid_domain could be contacted.", $errstr);
					return false;
				}
			} 
			
			// if we get here, we failed for some reason
			$this->status = array(false, 'NOT TESTED', "The email is valid, but none of the mailservers listed for $this->valid_domain could be contacted.", $errstr);
			return false;
			
		} else {
			
			// if we get here, then the email is validated AND verified 
			$this->status =  array(true, 'VALID', $this->valid, $response);
			return true;
		}
	}	
	
}
?>